Deep audit: data-safety, CLI, filter, and WPF UX hardening#2
Conversation
A user-protected folder was only guarded at its exact path. Because results are sorted parents-first and deleted recursively, an empty-eligible ancestor of a protected folder was deleted and took the protected child with it (the child was then counted as already-deleted-by-parent, never re-checked). This breached the per-folder protection invariant in both the direct and recycle-batch delete paths (AutoProtectRoot survived only because the root is the topmost item). A directory is now treated as protected when it, or any directory beneath it, is on the protected list (IsProtectedOrAncestorOfProtected), and the protected list is built with an OrdinalIgnoreCase comparer to match Windows path casing. Adds 6 regression tests covering self/ancestor/case-insensitive/prefix-sibling matching. Also: harden the cross-volume Move-to-folder fallback to re-verify file-free and delete the source bottom-up by handle instead of a blind recursive Directory.Delete; and derive the empty-files recycle batch outcome from !File.Exists rather than the sink's positional results.
- -saveprofile now rejects an unknown -mode, or -mode move without an absolute -moveto, with exit 1 instead of persisting a profile that fails every later -profile run. Profile names are trimmed and capped at 128 chars with control characters rejected (they corrupt -listprofiles output). - Config load restores any sub-object that a crafted file nil'd out, so the dirty-state check (incl. ConfigLoad's finally) can no longer NullReference and crash a headless run with a non-deterministic exit code. - The -eventlog summary now sanitizes folder names (bidi/zero-width/control) like the console and log lines already do.
…ching - A bare-name rule in a per-directory .gitignore (e.g. `dist`) was applied to every directory of that name tree-wide instead of only its own subtree, so unrelated directories were wrongly reported ignored/skipped in the dry-run. Name patterns now honor their scope like path patterns do (Git semantics). - User filter regexes (RegExName/RegExPath) had no match timeout, so a catastrophic-backtracking pattern could hang the scan thread. They now carry a one-second timeout and a timed-out match is treated as no-match. Adds a scoped-gitignore test and a ReDoS-timeout test.
Premium/accessibility pass on the default WPF shell: - The review list's computed status color was never bound to anything, so the destructive list had no color coding despite the legend promising it. Status is now a colored word (eligible=red, kept=muted, deleted=green, protected=blue, failed=amber) — color reinforces the word, never alone. - Text box, deletion-mode dropdown, filter lists, and result list now show a visible BlueLight keyboard-focus ring (were WPF's invisible dotted default). - A dry run no longer claims files were "changed": it reports "would be removed. Nothing was changed.", and completion copy matches the chosen mode. Canceled vs error-stopped runs read distinctly. - Legible red for eligible status text, comfortable row height, green/amber status indicator with a screen-reader-friendly name, a positive "no empty directories found" state, and one-off colors/radii folded onto the palette.
unprotectNode/ProtectNode cast node.Tag to DirectoryInfo unconditionally, so a node with a null or placeholder Tag threw while walking the tree. Both now use a pattern-matched guard and skip such nodes (the class's own TODO flagged this).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21ca0881dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (Options == null) Options = new ConfigOptions(); | ||
| if (Filters == null) Filters = new ConfigFilters(); |
There was a problem hiding this comment.
Initialize replacement config children with real defaults
When a crafted/corrupt config nils one of these children, the replacement constructors do not apply RED++'s normal defaults: ConfigOptions.SetToDefaults() is what sets values like MaxDirectoryDepth = -1, AutoProtectRoot, and the system/hidden-directory defaults, and ConfigFilters.SetToDefaults() seeds the ignore/never-empty lists. As written, a headless run with <Options xsi:nil="true"/> or <Filters xsi:nil="true"/> no longer crashes but continues with CLR zeroes or empty safety filters, so it can behave very differently from a fresh default config; call the relevant SetToDefaults() when creating these replacements.
Useful? React with 👍 / 👎.
A deep engineering + product-quality audit of RED++ (a Windows tool that recursively deletes empty directories). Five reviewable commits across data safety, CLI robustness, filtering correctness, and WPF UX/accessibility. Engine build clean, 58 tests (+8), headless safety smoke green, GUI verified by screenshot.
Correctness & data safety
IsProtectedOrAncestorOfProtected), and the list is matched case-insensitively. 6 regression tests.Directory.Delete.!File.Existsrather than the shell sink's positional results (matches the directory-path fix;IFileOperationdoesn't guarantee callback order).CLI & config robustness (Task Scheduler safety)
-saveprofilerejects an unknown-mode, or-mode movewithout an absolute-moveto, instead of persisting a profile that fails every later run. Profile names are trimmed, length-capped, and control-char-rejected.ConfigLoad'sfinally) can't NRE and crash a headless run.-eventlogsummary now sanitizes folder names (bidi/zero-width/control) like the log lines.Filtering correctness
.gitignore's bare-name rule (e.g.dist) is now scoped to its own subtree (Git semantics) instead of applying tree-wide, so unrelated directories aren't wrongly reported ignored/skipped in the dry-run.(a+)+$) can't hang the scan thread (ReDoS). 2 tests.WPF UX, accessibility & polish (default shell)
DirectoryInfonodes.Verification
dotnet build -c Releaseclean;dotnet test58/58.Deferred items (light/high-contrast theme for the WPF shell, undo-manifest path validation, filter separator normalization, tree protect/unprotect symmetry, minor hardening) are recorded in the local ROADMAP, not implemented here.